package gComEngine;

import generalObjects.BaseClientContract;
import generalObjects.CassandraHandler;
import generalObjects.GroupInformation;
import generalObjects.GroupView;
import groupManagementModule.GMMLayerAccess;

import java.rmi.ConnectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
import java.util.Properties;
import java.util.Queue;
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;

import javax.swing.event.ChangeListener;

import messageOrderingModule.MOMLayerAccess;
import nameServerPersistent.ServerRMI;

import com.datastax.driver.core.ConsistencyLevel;
import com.datastax.driver.core.ResultSet;
import communicationModule.CMLayerAccess;
import communicationModule.MessageObject;
import communicationModule.SendResult;

import debug.DebugLayerAccess;
import exceptions.NameTakenException;
import exceptions.NotDebugModeException;
import exceptions.NotLeaderException;

/**
 * The parent class of the GCom system. The model is responsible for creating
 * each module of the GCom stack, communication
 * module(CMLayerAccess),(optional)Debug module(DebugLayerAccess), Message
 * Ordering Module (MOMLayerAccess),Group Management Module(GMMLayerAccess).
 * This is the access point for the subsystem and public methods supplied by
 * this client is what's available in the communication with GCOM. When the
 * method {@link Model.processWork} is called, the calling thread will be used
 * to power the GCom subsystem. This provides an optional choice whether to run
 * the system on the main thread or on a workerthread.
 * 
 */
public class Model {

    private BaseClientContract ownStub;
    private BaseClientContract leaderStub;
    private final Properties sessionProp;
    private final String[] nameservers;
    private final GMMLayerAccess gMM;
    private MOMLayerAccess mOM;
    private CMLayerAccess cM;
    private DebugLayerAccess debugLayer;
    private final Queue<String[]> outputQueue;
    private GroupView groupView;
    private final ConcurrentHashMap<Long, Integer> receivedbuf;
    private String[] cassandraNodes;
    private final CassandraHandler cassHandler;
    private final ChangeListener groupUpdater;
    private final ChangeListener debugHoldbackUpdater;
    private final ChangeListener orderingHoldbackUpdater;
    private final ChangeListener receivedFilterUpdater;

    public Model(final Properties sessionProp,
            final Queue<String[]> outputQueue, final String[] nameservers,
            ChangeListener groupUpdater, ChangeListener debugHoldbackUpdater,
            ChangeListener orderingHoldbackUpdater,
            ChangeListener receivedFilterUpdater) throws RemoteException,
            NotBoundException, ServerNotActiveException, NameTakenException,
            NotLeaderException {
        this.nameservers = nameservers;
        this.sessionProp = sessionProp;
        this.outputQueue = outputQueue;
        receivedbuf = new ConcurrentHashMap<Long, Integer>();
        this.groupUpdater = groupUpdater;
        this.debugHoldbackUpdater = debugHoldbackUpdater;
        this.orderingHoldbackUpdater = orderingHoldbackUpdater;
        this.receivedFilterUpdater = receivedFilterUpdater;

        gMM = new GMMLayerAccess(sessionProp, this.nameservers, groupUpdater,
                receivedbuf);

        createClientReg();

        // gets the leaderstub from nameserver.
        nameServerResolve();
        cassHandler = new CassandraHandler(cassandraNodes, "GCom");
        gMM.setCassandraHandler(cassHandler);

        if (leaderStub == null) {
            // we're leader.
            leaderStub = ownStub;
            setupGroup();
        } else {
            joinGroup();
        }
        gMM.setGroupView(groupView);
        buildGComLayerChain();
    }

    public Vector<String> getGroupNicknames() {
        final Vector<String> nickList = new Vector<String>();
        for (final String key : groupView.getView().keySet()) {
            nickList.add(key);
        }
        return nickList;
    }

    private void setupGroup() {

        groupView = new GroupView();
        // final ClientInfo myinfo = new ClientInfo(0, ownStub);
        final String nick = sessionProp.getProperty("nickname");
        final String gName = sessionProp.getProperty("groupname");
        groupView.setLeader(nick);
        ResultSet result = cassHandler.executeStatement(
                "SELECT myseqnr FROM usersstamp WHERE groupname=? AND nick=?",
                ConsistencyLevel.QUORUM, gName, nick);
        int clock = 0;
        if (!result.isExhausted()) {
            clock = result.one().getInt("myseqnr");
        }
        sessionProp.setProperty("sendclock", "" + clock);
        groupView.appendMember(nick, ownStub, clock);
    }

    public void processWork() throws RemoteException, NotBoundException,
            ServerNotActiveException {
        groupUpdater.stateChanged(null);
        while (true) {
            // microsleep to prevent hogging cpu.
            try {
                Thread.sleep(5);
            } catch (final InterruptedException e) {
            }
            /*
             * start engine, think of the thread as a piston going through the
             * layers. Returns are kicked up to control for display in gui.
             */
            final List<SendResult> result = gMM.process();
            if (result != null) {
                for (final SendResult sr : result) {
                    outputQueue.offer(new String[] { sr.getMsg().getSender(),
                            sr.getMsg().getContent() });
                }
            }
        }
    }

    private void joinGroup() throws RemoteException, NameTakenException,
            NotLeaderException {
        final String nick = sessionProp.getProperty("nickname");
        final GroupInformation gInfo = leaderStub.join(ownStub, nick);
        sessionProp.setProperty("multicast", gInfo.getMulticastAlgorithm());
        sessionProp.setProperty("ordering", gInfo.getOrderingAlgorithm());
        groupView = gInfo.getGroupView();
        sessionProp.setProperty("sendclock", ""
                + groupView.getVectorClock().get(nick));
    }

    private void buildGComLayerChain() {

        mOM = new MOMLayerAccess(sessionProp, groupView,
                orderingHoldbackUpdater);
        cM = new CMLayerAccess(sessionProp, groupView, receivedbuf,
                receivedFilterUpdater);

        if (sessionProp.containsKey("debug")) {
            /* in debugmode we insert debug layer between mom and cm. */
            debugLayer = new DebugLayerAccess(sessionProp, debugHoldbackUpdater);

            mOM.setLowerLayer(debugLayer);
            debugLayer.setLowerLayer(cM);
        } else {
            mOM.setLowerLayer(cM);
        }
        /* mom always has gmm as higher layer. */
        gMM.setLowerLayer(mOM);

    }

    private void createClientReg() throws RemoteException {

        ownStub = (BaseClientContract) UnicastRemoteObject.exportObject(gMM, 0);
        // create registry at random port between 5000-65500
        LocateRegistry.createRegistry((new Random().nextInt(60500) + 5000));
    }

    private void nameServerResolve() throws RemoteException, NotBoundException,
            ServerNotActiveException {

        for (final String addr : nameservers) {
            try {
                final Registry registry = LocateRegistry.getRegistry(addr);
                final ServerRMI stub = (ServerRMI) registry
                        .lookup("Nameserver");
                // get leader stub
                leaderStub = (BaseClientContract) stub.join(
                        sessionProp.getProperty("groupname"), gMM);
                cassandraNodes = stub.getCassandraNodes();
                // gMM.setCassandraNodes(stub.getCassandraNodes());

                return;
            } catch (final RemoteException e) {
            }
            // if we reach here we've tried all and none worked.
        }
        throw new ConnectException(null);
    }

    public void sendMessage(final String msg) throws RemoteException {
        // increment sent clock.
        final int newclock = Integer.parseInt(sessionProp
                .getProperty("sendclock")) + 1;
        sessionProp.setProperty("sendclock", "" + newclock);

        // insert new sent clock value into this message
        final String nickname = sessionProp.getProperty("nickname");
        final GroupView tmpView = groupView.clone();

        // final ClientInfo myCinfo = tmpView.get(nickname);
        // myCinfo.setSequenceNr(newclock);
        // tmpView.put(nickname, myCinfo);
        tmpView.getVectorClock().put(nickname, newclock);

        // send our message to registry.
        ownStub.send(new MessageObject(nickname, "message", msg, tmpView, ""));
    }

    public void debugReleaseAll() throws NotDebugModeException {
        if (!sessionProp.containsKey("debug")) {
            throw new NotDebugModeException();
        }
        debugLayer.releaseAll();

    }

    public void debugRelease(final MessageObject msg)
            throws NotDebugModeException {
        if (!sessionProp.containsKey("debug")) {
            throw new NotDebugModeException();
        }
        debugLayer.release(msg);
    }

    public Object[] debugGetDebugHoldback() throws NotDebugModeException {
        if (!sessionProp.containsKey("debug")) {
            throw new NotDebugModeException();
        }
        return debugLayer.getQueue();
    }

    public Object[] debugGetOrderingHoldback() throws NotDebugModeException {
        if (!sessionProp.containsKey("debug")) {
            throw new NotDebugModeException();
        }
        return mOM.getHoldbackQueue();
    }

    public Vector<String> debugGetReceivedBuffer() throws NotDebugModeException {
        if (!sessionProp.containsKey("debug")) {
            throw new NotDebugModeException();
        }
        return cM.getReceivedBufferStrings();
    }

}
